/*
 * ﻿Copyright (C) 2013-2014 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.injection;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.FieldSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is another version of <code>{@link AbstractSwapperAspect}</code> aspect in order to use 
 * <code>{@link Swap &#x40;Swap}</code> annotation and point-cut to specify target rather than just 
 * by point-cut.<br /> 
 * @see AbstractSwapperAspect
 * 
 * @author Arata Y.
 */
@Aspect
public abstract class AbstractAnnoSwapperAspect {
	private Logger logger = LoggerFactory.getLogger( this.getClass());
		public Logger getLogger() {
			return logger;
		}
		protected void setLogger( final Logger logger) {
			this.logger = logger;
		}
	
	/**
	 * To provide alternate object for swapping original one what will be obtained by quoting target 
	 * field value or target method invocation. <br />
	 * Sub (non abstract) aspect must override this.<br />
	 * 
	 * @return Alternate object being swapped for original one.
	 * @see #aroundGettingOriginal(ProceedingJoinPoint, Swap)
	 */
	public abstract Object getReplacement();
	
	/**
	 * Abstract point-cut for narrowing scope of picking up quoting target field value or target method 
	 * invocation in order to swap its result value.<br />
	 * The value will be swapped to one being returned by <code>{@link #getReplacement()}</code> method 
	 * in <code>{@link #aroundGettingOriginal(ProceedingJoinPoint, Swap)}</code> advise method. <br />
	 * Sub (non-abstract) aspect must override this.
	 */
	@Pointcut
	public abstract void pointcutToScreenSnapperScope();
	
	/**
	 * Point-cut cross-cut join-point with <code>{@link Swap &#x40;Swap}</code> annotation. <br />
	 * Its scope will be narrowed down by <code>{@link #pointcutToScreenSnapperScope()}</code> point-cut.
	 * 
	 * @param swap
	 */
	@Pointcut( value = "@annotation( swap) && pointcutToScreenSnapperScope()", argNames = "swap")
	public void pointcutWithSwapAnnotation( final Swap swap) {}
	
	protected void throwProperly( final Throwable throwable, final Method method) throws Throwable {
		if ( RuntimeException.class.isInstance( throwable)) {
			throw (RuntimeException)throwable;
		}
		else {
			if ( method != null) {
				for( Class<?> exceptionType : method.getExceptionTypes()) {
					if ( !exceptionType.isInstance( throwable)) continue;
					throw throwable;
				}
			}
			throw new RuntimeException( throwable);
		}
	}
	
	protected Object proceed( final ProceedingJoinPoint proceedingJoinPoint, final Method method) 
	throws Throwable {
		Object result = null;
			try {
				if ( proceedingJoinPoint.getArgs().length < 1) {
					result = proceedingJoinPoint.proceed();
				}
				else {
					result = proceedingJoinPoint.proceed( proceedingJoinPoint.getArgs());
				}
			}
			catch( Throwable throwable) {
				throwProperly( throwable, method);
			}
		return result;
	}
	
	/**
	 * Around advise method to swap result object (what will be obtained by quoting target field value or 
	 * target method invocation) to one returned by <code>{@link #getReplacement()}</code> method.<br />
	 * 
	 * @param proceedingJoinPoint : Join-point by point-cut specified with 
	 * <code>{@link #pointcutWithSwapAnnotation(Swap)}</code> point-cut.
	 * @param swap : Swap annotation instance found at join-point.
	 * @return Either object returned by <code>{@link #getReplacement()}</code> method or original object 
	 * (what is being obtained by quoting target field value or target method invocation) according 
	 * to the setting with <code>{@link Swap#isOnlyForNull()}</code> annotation attribute value.
	 */
	@Around( value = "pointcutWithSwapAnnotation( swap)", argNames = "swap")
	public Object aroundGettingOriginal( 
			final ProceedingJoinPoint proceedingJoinPoint, final Swap swap) throws Throwable {
		final Logger logger = getLogger();
		
		Field field = null;
		Method method = null;
			final Signature signature = proceedingJoinPoint.getSignature();
			if ( FieldSignature.class.isInstance( signature)) {
				field = ((FieldSignature)signature).getField();
			}
			else if ( MethodSignature.class.isInstance( signature)) {
				final MethodSignature methodSignature = (MethodSignature)signature;
				method = methodSignature.getMethod();
			}
			
		Object resultObj = null;
			if ( ( field == null) && ( method == null)) {
				resultObj = proceed( proceedingJoinPoint, method);
				if ( logger.isInfoEnabled()) {
					logger.info(
							String.format(
									"Unsupported join-point was picked up by given point-cut; currently " 
									+ "%1$s aspect supports only either obtaining value from field with " 
									+ "@%2$s annotation or invocation of (getter) method with @%2$s " 
									+ "annotation. Thereby, just proceeding without swapping target value. " 
									+ "Join-point picked up : %3$s.",
									this.getClass().getSimpleName(),
									Swap.class.getSimpleName(),
									proceedingJoinPoint.toString())
							);
				}
			}
			else {
				final Object replacement = getReplacement();
				boolean isPointcutValid = true;
					if ( replacement != null) {
						if ( ( field != null) && ( !field.getType().isInstance( replacement))) {
							isPointcutValid = false;
							
							if ( logger.isInfoEnabled()) {
								logger.info(
										String.format(
												"Integrity mismatch: type of field at join-point " 
												+ "what picked up by given point-cut and type " 
												+ "of object swapping that field value did not " 
												+ "match. Thereby, just proceeding without " 
												+ "swapping field value." 
												+ "%nField: %1$s." 
												+ "%nReplacement swapping return value: %2$s." 
												+ "%nJoin-point: %3$s.",
												field,
												replacement,
												proceedingJoinPoint.toString())
										);
							}
						}
						else if ( ( method != null) 
								&& ( !method.getReturnType().isInstance( replacement))) {
							isPointcutValid = false;
							
							if ( logger.isInfoEnabled()) {
								logger.info(
										String.format(
												"Integrity mismatch: return type of waved method " 
												+ "at join-pointwhat picked up by given " 
												+ "point-cut and type of object swapping return " 
												+ "value of that method did not match. Thereby, " 
												+ "just proceeding without swapping return value." 
												+ "%nMethod: %1$s." 
												+ "%nReplacement swapping return value: %2$s." 
												+ "%nJoin-point: %3$s.",
												method,
												replacement,
												proceedingJoinPoint.toString())
										);
							}
						}
					}
				
				if ( !isPointcutValid) {
					resultObj = proceed( proceedingJoinPoint, method);
				}
				else {
					if ( !swap.isOnlyForNull()) {
						resultObj = replacement;
					}
					else {
						resultObj = proceed( proceedingJoinPoint, method);
							if ( resultObj == null) {
								resultObj = replacement;
							}
					}
				}
			}
		
			if ( logger.isDebugEnabled()) {
				logger.debug(
						String.format(
								"Returning %1$s as result of proceeding the join-point \"%2$s\".",
								resultObj, 
								proceedingJoinPoint.toString())
						);
			}
		
		return resultObj;
	}
}
//TODO add constructor pattern